/*
Copyright Paul James Mutton, 2001-2003, http://www.jibble.org/

This file is part of EpsGraphics2D.

This software is free for personal, educational and non-commercial
use only. A license is available for commercial use. An unlimited
license is also available, allowing you to distribute this software
as part of your own applications and to modify the source code as
you see fit.

This source code, or parts thereof, may not be distributed in any
modified form without permission from the author.

EpsGraphics2D is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$Author: pjm2 $
$Id: EpsGraphics2D.java,v 1.33 2003/12/02 14:28:28 pjm2 Exp $
*/

package org.jibble.epsgraphics;

import java.awt.*;
import java.awt.image.*;
import java.awt.image.renderable.*;
import java.awt.geom.*;
import java.io.*;
import java.text.*;
import java.awt.font.*;
import java.util.*;


/**
 * EpsGraphics2D is suitable for creating high quality EPS graphics for
 * use in documents and papers, and can be used just like a standard
 * Graphics2D object.
 *  <p>
 * Many Java programs use Graphics2D to draw stuff on the screen, and while
 * it is easy to save the output as a png or jpeg file, it is a little
 * harder to export it as an EPS for including in a document or paper.
 *  <p>
 * This class makes the whole process extremely easy, because you can use
 * it as if it's a Graphics2D object.  The only difference is that all of
 * the implemented methods create EPS output, which means the diagrams you
 * draw can be resized without leading to any of the jagged edges you may
 * see when resizing pixel-based images, such as jpeg and png files.
 *  <p>
 *   Example usage:
 *  <p>
 * <pre>    Graphics2D g = new EpsGraphics2D();
 *    g.setColor(Color.black);
 *    
 *    // Line thickness 2.
 *    g.setStroke(new BasicStroke(2.0f));
 *    
 *    // Draw a line.
 *    g.drawLine(10, 10, 50, 10);
 *
 *    // Fill a rectangle in blue
 *    g.setColor(Color.blue);
 *    g.fillRect(10, 0, 20, 20);
 *
 *    // Get the EPS output.
 *    String output = g.toString();</pre>
 *  <p>
 * You do not need to worry about the size of the canvas when drawing on a
 * EpsGraphics2D object.  The bounding box of the EPS document will
 * automatically resize to accomodate new items that you draw.
 *  <p>
 * Not all methods are implemented yet.  Those that are not are clearly
 * labelled.
 *  <p>
 * Copyright Paul Mutton,
 *           <a href="http://www.jibble.org/">http://www.jibble.org/</a>
 * 
 * @author  $Author: pjm2 $
 * @version $Id: EpsGraphics2D.java,v 1.33 2003/12/02 14:28:28 pjm2 Exp $
 */
public class EpsGraphics2D extends java.awt.Graphics2D {
    
    
    public static final String VERSION = "0.4.0";


    /**
     * Constructs a new EPS document that is initially empty and can be
     * drawn on like a Graphics2D object.  The EPS document is stored in
     * memory.
     */
    public EpsGraphics2D() {
        this("Untitled");
    }

    
    /**
     * Constructs a new EPS document that is initially empty and can be
     * drawn on like a Graphics2D object.  The EPS document is stored in
     * memory.
     */
    public EpsGraphics2D(String title) {
        _document = new EpsDocument(title);
        _backgroundColor = Color.white;
        _clip = null;
        _transform = new AffineTransform();
        _accurateTextMode = true;
        setColor(Color.black);
        setPaint(Color.black);
        setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR));
        setFont(Font.decode(null));
        setStroke(new BasicStroke());
    }
    
    
    /**
     * Constructs a new EPS document that is initially empty and can be
     * drawn on like a Graphics2D object. The EPS document is written to
     * the ouput File as it goes, which reduces memory usage.  The
     * header of the file is updated with correct bounding information and
     * the file is closed when the close() method is called.
     */
    public EpsGraphics2D(String title, File output, int minX, int minY, int maxX, int maxY) throws IOException {
        this(title);
        _document = new EpsDocument(title, output, minX, minY, maxX, maxY);
    }
    
    
    /**
     * Constructs a new EpsGraphics2D instance that is a copy of the
     * supplied argument and points at the same EpsDocument.
     */
    protected EpsGraphics2D(EpsGraphics2D g) {
        _document = g._document;
        _backgroundColor = g._backgroundColor;
        _clip = g._clip;
        _transform = (AffineTransform) g._transform.clone();
        _color = g._color;
        _paint = g._paint;
        _composite = g._composite;
        _font = g._font;
        _stroke = g._stroke;
        _accurateTextMode = g._accurateTextMode;
    }
    
    
    /**
     * This method is called to indicate that a particular method is not
     * supported yet.  The stack trace is printed to the standard output.
     */
    private void methodNotSupported() {
        EpsException e = new EpsException("Method not currently supported by EpsGraphics2D version " + VERSION);
        e.printStackTrace(System.err);
    }
    
    
    /////////////// Specialist methods ///////////////////////
    
    
    /**
     * Sets whether to use accurate text mode when rendering text in EPS.
     * This is enabled (true) by default. When accurate text mode is used,
     * all text will be rendered in EPS to appear exactly the same as it
     * would do when drawn with a Graphics2D context. With accurate text
     * mode enabled, it is not necessary for the EPS viewer to have the
     * required font installed.
     * <p>
     * Turning off accurate text mode will require the EPS viewer to have
     * the necessary fonts installed. If you are using a lot of text, you
     * will find that this significantly reduces the file size of your EPS
     * documents.  AffineTransforms can only affect the starting point of
     * text using this simpler text mode - all text will be horizontal.
     */
    public void setAccurateTextMode(boolean b) {
        _accurateTextMode = b;
    }
    
    
    /**
     * Returns whether accurate text mode is being used.
     */
    public boolean getAccurateTextMode() {
        return _accurateTextMode;
    }
    
    
    /**
     * Closes the EPS file if you have opted to write the document directly
     * to disk. If you forget to do this, the file may be incomplete.
     */
    public void close() throws IOException {
        _document.close();
    }
    
    
    /**
     * Appends a line to the EpsDocument.
     */
    private void append(String line) {
        _document.append(this, line);
    }
    
    
    /**
     * Returns the point after it has been transformed by the transformation.
     */
    private Point2D transform(float x, float y) {
        Point2D result = new Point2D.Float(x, y);
        result = _transform.transform(result, result);
        result.setLocation(result.getX(), -result.getY());
        return result;
    }
    
    
    /**
     * Appends the commands required to draw a shape on the EPS document.
     */
    private void draw(Shape s, String action) {
        
        if (s != null) {
            
            if (!_transform.isIdentity()) {
                s = _transform.createTransformedShape(s);
            }
            
            // Update the bounds.
            if (!action.equals("clip")) {
                Rectangle2D shapeBounds = s.getBounds2D();
                Rectangle2D visibleBounds = shapeBounds;
                if (_clip != null) {
                    Rectangle2D clipBounds = _clip.getBounds2D();
                    visibleBounds = shapeBounds.createIntersection(clipBounds);
                }
                float lineRadius = _stroke.getLineWidth() / 2;
                float minX = (float) visibleBounds.getMinX() - lineRadius;
                float minY = (float) visibleBounds.getMinY() - lineRadius;
                float maxX = (float) visibleBounds.getMaxX() + lineRadius;
                float maxY = (float) visibleBounds.getMaxY() + lineRadius;
                _document.updateBounds(minX, -minY);
                _document.updateBounds(maxX, -maxY);
            }
            
            append("newpath");
            int type = 0;
            float[] coords = new float[6];
            PathIterator it = s.getPathIterator(null);
            while (!it.isDone()) {
                type = it.currentSegment(coords);
                float x1 = coords[0];
                float y1 = -coords[1];
                float x2 = coords[2];
                float y2 = -coords[3];
                float x3 = coords[4];
                float y3 = -coords[5];
                
                if (type == PathIterator.SEG_CLOSE) {
                    append("closepath");
                }
                else if (type == PathIterator.SEG_CUBICTO) {
                    append(x1 + " " + y1 + " " + x2 + " " + y2 + " " + x3 + " " + y3 + " curveto");
                }
                else if (type == PathIterator.SEG_LINETO) {
                    append(x1 + " " + y1 + " lineto");
                }
                else if (type == PathIterator.SEG_MOVETO) {
                    append(x1 + " " + y1 + " moveto");
                }
                else if (type == PathIterator.SEG_QUADTO) {
                    append(x1 + " " + y1 + " " + x1 + " " + y1 + " " + x2 + " " + y2 + " curveto");
                }
                else if (type == PathIterator.WIND_EVEN_ODD) {
                    // Ignore.
                }
                else if (type == PathIterator.WIND_NON_ZERO) {
                    // Ignore.
                }
                it.next();
            }
            append(action);
            append("newpath");
            
        }
    }
    
    
    /**
     * Returns a hex string that always contains two characters.
     */
    private String toHexString(int n) {
        String result = Integer.toString(n, 16);
        while (result.length() < 2) {
            result = "0" + result;
        }
        return result;
    }
    
    
    /////////////// Graphics2D methods ///////////////////////
    
    
    /**
     * Draws a 3D rectangle outline.  If it is raised, light appears to come
     * from the top left.
     */
    public void draw3DRect(int x, int y, int width, int height, boolean raised) {
        Color originalColor = getColor();
        Stroke originalStroke = getStroke();
        
        setStroke(new BasicStroke(1.0f));
        
        if (raised) {
            setColor(originalColor.brighter());
        }
        else {
            setColor(originalColor.darker());
        }
        
        drawLine(x, y, x + width, y);
        drawLine(x, y, x, y + height);
        
        if (raised) {
            setColor(originalColor.darker());
        }
        else {
            setColor(originalColor.brighter());
        }
        
        drawLine(x + width, y + height, x, y + height);
        drawLine(x + width, y + height, x + width, y);
        
        setColor(originalColor);
        setStroke(originalStroke);
    }


    /**
     * Fills a 3D rectangle.  If raised, it has bright fill and light appears
     * to come from the top left.
     */
    public void fill3DRect(int x, int y, int width, int height, boolean raised) {
        Color originalColor = getColor();
        
        if (raised) {
            setColor(originalColor.brighter());
        }
        else {
            setColor(originalColor.darker());
        }
        draw(new Rectangle(x, y, width, height), "fill");
        setColor(originalColor);
        draw3DRect(x, y, width, height, raised);
    }
    
    
    /**
     * Draws a Shape on the EPS document.
     */
    public void draw(Shape s) {
        draw(s, "stroke");
    }
    
    
    /**
     * <b><i><font color="red">Not implemented</font></i></b> - performs no action.
     */
    public boolean drawImage(Image img, AffineTransform xform, ImageObserver obs) {
        methodNotSupported();
        return true;
    }
    
    
    /**
     * <b><i><font color="red">Not implemented</font></i></b> - performs no action.
     */
    public void drawImage(BufferedImage img, BufferedImageOp op, int x, int y) {
        methodNotSupported();
    }
    
    
    /**
     * <b><i><font color="red">Not implemented</font></i></b> - performs no action.
     */
    public void drawRenderedImage(RenderedImage img, AffineTransform xform) {
        methodNotSupported();
    }
    
    
    /**
     * <b><i><font color="red">Not implemented</font></i></b> - performs no action.
     */
    public void drawRenderableImage(RenderableImage img, AffineTransform xform) {
        methodNotSupported();
    }
    
    
    /**
     * Draws a string at (x,y)
     */
    public void drawString(String str, int x, int y) {
        drawString(str, (float) x, (float) y);
    }
    
    
    /**
     * Draws a string at (x,y)
     */
    public void drawString(String s, float x, float y) {
        if (s != null && s.length() > 0) {
            AttributedString as = new AttributedString(s);
            as.addAttribute(TextAttribute.FONT, getFont());
            drawString(as.getIterator(), x, y);
        }
    }
    
    
    /**
     * Draws the characters of an AttributedCharacterIterator, starting from
     * (x,y).
     */
    public void drawString(AttributedCharacterIterator iterator, int x, int y) {
        drawString(iterator, (float) x, (float) y);
    }
    
    
    /**
     * Draws the characters of an AttributedCharacterIterator, starting from
     * (x,y).
     */
    public void drawString(AttributedCharacterIterator iterator, float x, float y) {
        if (getAccurateTextMode()) {
            TextLayout layout = new TextLayout(iterator, getFontRenderContext());
            Shape shape = layout.getOutline(AffineTransform.getTranslateInstance(x, y));
            draw(shape, "fill");
        }
        else {
            append("newpath");
            Point2D location = transform(x, y);
            append(location.getX() + " " + location.getY() + " moveto");
            StringBuffer buffer = new StringBuffer();
            for (char ch = iterator.first(); ch != CharacterIterator.DONE; ch = iterator.next()) {
                if (ch == '(' || ch == ')') {
                    buffer.append('\\');
                }
                buffer.append(ch);
            }
            append("(" + buffer.toString() + ") show");
        }
    }
    
    
    /**
     * Draws a GlyphVector at (x,y)
     */
    public void drawGlyphVector(GlyphVector g, float x, float y) {
        Shape shape = g.getOutline(x, y);
        draw(shape, "fill");
    }
    
    
    /**
     * Fills a Shape on the EPS document.
     */
    public void fill(Shape s) {
        draw(s, "fill");
    }
    
    
    /**
     * Checks whether or not the specified Shape intersects the specified
     * Rectangle, which is in device space.
     */
    public boolean hit(Rectangle rect, Shape s, boolean onStroke) {
        return s.intersects(rect);
    }
    
    
    /**
     * Returns the device configuration associated with this EpsGraphics2D
     * object.
     */
    public GraphicsConfiguration getDeviceConfiguration() {
        GraphicsConfiguration gc = null;
        GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
        GraphicsDevice[] gds = ge.getScreenDevices();
        for (int i = 0; i < gds.length; i++) {
            GraphicsDevice gd = gds[i];
            GraphicsConfiguration[] gcs = gd.getConfigurations();
            if (gcs.length > 0) {
                return gcs[0];
            }
        }
        return gc;
    }
    
    
    /**
     * Sets the Composite to be used by this EpsGraphics2D.  EpsGraphics2D
     * does not make use of these.
     */
    public void setComposite(Composite comp) {
        _composite = comp;
    }
    
    
    /**
     * Sets the Paint attribute for the EpsGraphics2D object.  Only Paint
     * objects of type Color are respected by EpsGraphics2D.
     */
    public void setPaint(Paint paint) {
        _paint = paint;
        if (paint instanceof Color) {
            setColor((Color) paint);
        }
    }
    
    
    /**
     * Sets the stroke.  Only accepts BasicStroke objects (or subclasses of
     * BasicStroke).
     */
    public void setStroke(Stroke s) {
        if (s instanceof BasicStroke) {
            _stroke = (BasicStroke) s;
            
            append(_stroke.getLineWidth() + " setlinewidth");
            append(_stroke.getMiterLimit() + " setmiterlimit");
            append(_stroke.getLineJoin() + " setlinejoin");
            append(_stroke.getEndCap() + " setlinecap");
            
            StringBuffer dashes = new StringBuffer();
            dashes.append("[ ");
            float[] dashArray = _stroke.getDashArray();
            if (dashArray != null) {
                for (int i = 0; i < dashArray.length; i++) {
                    dashes.append((dashArray[i]) + " ");
                }
            }
            dashes.append("]");
            append(dashes.toString() + " 0 setdash");
        }
    }
    
    
    /**
     * Sets a rendering hint. These are not used by EpsGraphics2D.
     */
    public void setRenderingHint(RenderingHints.Key hintKey, Object hintValue) {
        // Do nothing.
    }
    
    
    /**
     * Returns the value of a single preference for the rendering
     * algorithms.  Rendering hints are not used by EpsGraphics2D.
     */
    public Object getRenderingHint(RenderingHints.Key hintKey) {
        return null;
    }
    
    
    /**
     * Sets the rendering hints.  These are ignored by EpsGraphics2D.
     */
    public void setRenderingHints(Map hints) {
        // Do nothing.
    }
    
    
    /**
     * Adds rendering hints.  These are ignored by EpsGraphics2D.
     */
    public void addRenderingHints(Map hints) {
        // Do nothing.
    }
    
    
    /**
     * Returns the preferences for the rendering algorithms.
     */
    public RenderingHints getRenderingHints() {
        return new RenderingHints(null);
    }
    
    
    /**
     * Translates the origin of the EpsGraphics2D context to the point (x,y)
     * in the current coordinate system.
     */
    public void translate(int x, int y) {
        translate((double) x, (double) y);
    }
    
    
    /**
     * Concatenates the current EpsGraphics2D Transformation with a
     * translation transform.
     */
    public void translate(double tx, double ty) {
        transform(AffineTransform.getTranslateInstance(tx, ty));
    }
    
    
    /**
     * Concatenates the current EpsGraphics2D Transform with a rotation
     * transform.
     */
    public void rotate(double theta) {
        rotate(theta, 0, 0);
    }
    
    
    /**
     * Concatenates the current EpsGraphics2D Transform with a translated
     * rotation transform.
     */
    public void rotate(double theta, double x, double y) {
        transform(AffineTransform.getRotateInstance(theta, x, y));
    }
    
    
    /**
     * Concatenates the current EpsGraphics2D Transform with a scaling
     * transformation.
     */
    public void scale(double sx, double sy) {
        transform(AffineTransform.getScaleInstance(sx, sy));
    }
    
    
    /**
     * Concatenates the current EpsGraphics2D Transform with a shearing
     * transform.
     */
    public void shear(double shx, double shy) {
        transform(AffineTransform.getShearInstance(shx, shy));
    }
    
    
    /**
     * Composes an AffineTransform object with the Transform in this
     * EpsGraphics2D according to the rule last-specified-first-applied.
     */
    public void transform(AffineTransform Tx) {
        _transform.concatenate(Tx);
        setTransform(getTransform());
    }
    
    
    /**
     * Sets the AffineTransform to be used by this EpsGraphics2D.
     */
    public void setTransform(AffineTransform Tx) {
        if (Tx == null) {
            Tx = new AffineTransform();
        }
        _transform = Tx;
        // Need to update the stroke and font so they know the scale changed
        setStroke(getStroke());
        setFont(getFont());
    }
    
    
    /**
     * Gets the AffineTransform used by this EpsGraphics2D.
     */
    public AffineTransform getTransform() {
        return new AffineTransform(_transform);
    }
    
    
    /**
     * Returns the current Paint of the EpsGraphics2D object.
     */
    public Paint getPaint() {
        return _paint;
    }
    
    
    /**
     * returns the current Composite of the EpsGraphics2D object.
     */
    public Composite getComposite() {
        return _composite;
    }
    
    
    /**
     * Sets the background color to be used by the clearRect method.
     */
    public void setBackground(Color color) {
        if (color == null) {
            color = Color.black;
        }
        _backgroundColor = color;
    }
    
    
    /**
     * Gets the background color that is used by the clearRect method.
     */
    public Color getBackground() {
        return _backgroundColor;
    }
    
    
    /**
     * Returns the Stroke currently used.  Guaranteed to be an instance of
     * BasicStroke.
     */
    public Stroke getStroke() {
        return _stroke;
    }
    
    
    /**
     * Intersects the current clip with the interior of the specified Shape
     * and sets the clip to the resulting intersection.
     */
    public void clip(Shape s) {
        if (_clip == null) {
            setClip(s);
        }
        else {
            draw(s, "clip");
            Area area = new Area(_clip);
            area.intersect(new Area(s));
            _clip = area;
        }
    }
    
    
    /**
     * Returns the FontRenderContext.
     */
    public FontRenderContext getFontRenderContext() {
        return _fontRenderContext;
    }
    
    
    /////////////// Graphics methods ///////////////////////
    
    
    /**
     * Returns a new Graphics object that is identical to this EpsGraphics2D.
     */
    public Graphics create() {
        return new EpsGraphics2D(this);
    }
    
    
    /**
     * Returns an EpsGraphics2D object based on this 
     * Graphics object, but with a new translation and clip
     * area.
     */
    public Graphics create(int x, int y, int width, int height) {
	Graphics g = create();
	g.translate(x, y);
	g.clipRect(0, 0, width, height);
	return g;
    }
    
    
    /**
     * Returns the current Color.  This will be a default value (black)
     * until it is changed using the setColor method.
     */
    public Color getColor() {
        return _color;
    }
    
    
    /**
     * Sets the Color to be used when drawing all future shapes, text, etc.
     */
    public void setColor(Color c) {
        if (c == null) {
            c = Color.black;
        }
        _color = c;
        append((c.getRed() / 255f) + " " + (c.getGreen() / 255f) + " " + (c.getBlue() / 255f) + " setrgbcolor");
    }
    
    
    /**
     * Sets the paint mode of this EpsGraphics2D object to overwrite the
     * destination EpsDocument with the current color.
     */
    public void setPaintMode() {
        // Do nothing - paint mode is the only method supported anyway.
    }
    
    
    /**
     * <b><i><font color="red">Not implemented</font></i></b> - performs no action.
     */
    public void setXORMode(Color c1) {
        methodNotSupported();
    }
    
    
    /**
     * Returns the Font currently being used.
     */
    public Font getFont() {
        return _font;
    }
    
    
    /**
     * Sets the Font to be used in future text.
     */
    public void setFont(Font font) {
        if (font == null) {
            font = Font.decode(null);
        }
        _font = font;
        append("/" + _font.getPSName() + " findfont " + ((int) _font.getSize()) + " scalefont setfont");
    }
    
    
    /**
     * Gets the font metrics of the current font.
     */
    public FontMetrics getFontMetrics() {
        return getFontMetrics(getFont());
    }
    
    
    /**
     * Gets the font metrics for the specified font.
     */
    public FontMetrics getFontMetrics(Font f) {
        BufferedImage image = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
        Graphics g = image.getGraphics();
        return g.getFontMetrics(f);
    }
    
    
    /**
     * Returns the bounding rectangle of the current clipping area.
     */
    public Rectangle getClipBounds() {
        if (_clip == null) {
            return null;
        }
        Rectangle rect = _clip.getBounds();
        return rect;
    }
    
    
    /**
     * Intersects the current clip with the specified rectangle.
     */
    public void clipRect(int x, int y, int width, int height) {
        clip(new Rectangle(x, y, width, height));
    }
    
    
    /**
     * Sets the current clip to the rectangle specified by the given
     * coordinates.
     */
    public void setClip(int x, int y, int width, int height) {
        setClip(new Rectangle(x, y, width, height));
    }
    
    
    /**
     * Gets the current clipping area.
     */
    public Shape getClip() {
        return _clip;
    }
    
    
    /**
     * Sets the current clipping area to an arbitrary clip shape.
     */
    public void setClip(Shape clip) {
        if (clip != null) {
            if (_document.isClipSet()) {
                append("grestore");
                append("gsave");
            }
            else {
                _document.setClipSet(true);
                append("gsave");
            }
            draw(clip, "clip");
            _clip = clip;
        }
        else {
            if (_document.isClipSet()) {
                append("grestore");
                _document.setClipSet(false);
            }
            _clip = null;
        }
    }
    
    
    /**
     * <b><i><font color="red">Not implemented</font></i></b> - performs no action.
     */
    public void copyArea(int x, int y, int width, int height, int dx, int dy) {
        methodNotSupported();
    }
    
    
    /**
     * Draws a straight line from (x1,y1) to (x2,y2).
     */
    public void drawLine(int x1, int y1, int x2, int y2) {
        Shape shape = new Line2D.Float(x1, y1, x2, y2);
        draw(shape);
    }
    
    
    /**
     * Fills a rectangle with top-left corner placed at (x,y).
     */
    public void fillRect(int x, int y, int width, int height) {
        Shape shape = new Rectangle(x, y, width, height);
        draw(shape, "fill");
    }
    
    
    /**
     * Draws a rectangle with top-left corner placed at (x,y).
     */
    public void drawRect(int x, int y, int width, int height) {
        Shape shape = new Rectangle(x, y, width, height);
        draw(shape);
    }
    
    
    /**
     * Clears a rectangle with top-left corner placed at (x,y) using the
     * current background color.
     */
    public void clearRect(int x, int y, int width, int height) {
        Color originalColor = getColor();
        
        setColor(getBackground());
        Shape shape = new Rectangle(x, y, width, height);
        draw(shape, "fill");
        
        setColor(originalColor);
    }
    
    
    /**
     * Draws a rounded rectangle.
     */
    public void drawRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) {
        Shape shape = new RoundRectangle2D.Float(x, y, width, height, arcWidth, arcHeight);
        draw(shape);
    }
    
    
    /**
     * Fills a rounded rectangle.
     */
    public void fillRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) {
        Shape shape = new RoundRectangle2D.Float(x, y, width, height, arcWidth, arcHeight);
        draw(shape, "fill");
    }
    
    
    /**
     * Draws an oval.
     */
    public void drawOval(int x, int y, int width, int height) {
        Shape shape = new Ellipse2D.Float(x, y, width, height);
        draw(shape);
    }
    
    
    /**
     * Fills an oval.
     */
    public void fillOval(int x, int y, int width, int height) {
        Shape shape = new Ellipse2D.Float(x, y, width, height);
        draw(shape, "fill");
    }
    
    
    /**
     * Draws an arc.
     */
    public void drawArc(int x, int y, int width, int height, int startAngle, int arcAngle) {
        Shape shape = new Arc2D.Float(x, y, width, height, startAngle, arcAngle, Arc2D.OPEN);
        draw(shape);
    }
    
    
    /**
     * Fills an arc.
     */
    public void fillArc(int x, int y, int width, int height, int startAngle, int arcAngle) {
        Shape shape = new Arc2D.Float(x, y, width, height, startAngle, arcAngle, Arc2D.PIE);
        draw(shape, "fill");
    }
    
    
    /**
     * Draws a polyline.
     */
    public void drawPolyline(int[] xPoints, int[] yPoints, int nPoints) {
        if (nPoints > 0) {
            GeneralPath path = new GeneralPath();
            path.moveTo(xPoints[0], yPoints[0]);
            for (int i = 1; i < nPoints; i++) {
                path.lineTo(xPoints[i], yPoints[i]);
            }
            draw(path);
        }
    }
    
    
    /**
     * Draws a polygon made with the specified points.
     */
    public void drawPolygon(int[] xPoints, int[] yPoints, int nPoints) {
        Shape shape = new Polygon(xPoints, yPoints, nPoints);
        draw(shape);
    }
    
    
    /**
     * Draws a polygon.
     */
    public void drawPolygon(Polygon p) {
        draw(p);
    }
    
    
    /**
     * Fills a polygon made with the specified points.
     */
    public void fillPolygon(int[] xPoints, int[] yPoints, int nPoints) {
        Shape shape = new Polygon(xPoints, yPoints, nPoints);
        draw(shape, "fill");
    }
    
    
    /**
     * Fills a polygon.
     */
    public void fillPolygon(Polygon p) {
        draw(p, "fill");
    }
    
    
    /**
     * Draws the specified characters, starting from (x,y)
     */
    public void drawChars(char[] data, int offset, int length, int x, int y) {
        String string = new String(data, offset, length);
        drawString(string, x, y);
    }
    
    
    /**
     * Draws the specified bytes, starting from (x,y)
     */
    public void drawBytes(byte[] data, int offset, int length, int x, int y) {
        String string = new String(data, offset, length);
        drawString(string, x, y);
    }
    
    
    /**
     * Draws an image.
     */
    public boolean drawImage(Image img, int x, int y, ImageObserver observer) {
        return drawImage(img, x, y, Color.white, observer);
    }
    
    
    /**
     * Draws an image.
     */
    public boolean drawImage(Image img, int x, int y, int width, int height, ImageObserver observer) {
        return drawImage(img, x, y, width, height, Color.white, observer);
    }
    
    
    /**
     * Draws an image.
     */
    public boolean drawImage(Image img, int x, int y, Color bgcolor, ImageObserver observer) {
        int width = img.getWidth(null);
        int height = img.getHeight(null);
        return drawImage(img, x, y, width, height, bgcolor, observer);
    }
    
    
    /**
     * Draws an image.
     */
    public boolean drawImage(Image img, int x, int y, int width, int height, Color bgcolor, ImageObserver observer) {
        return drawImage(img, x, y, x + width, y + height, 0, 0, width, height, bgcolor, observer);
    }
    
    
    /**
     * Draws an image.
     */
    public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2, ImageObserver observer) {
        return drawImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, Color.white, observer);
    }
    
    
    /**
     * Draws an image.
     */
    public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2, Color bgcolor, ImageObserver observer) {
        if (dx1 >= dx2) {
            throw new IllegalArgumentException("dx1 >= dx2");
        }
        if (sx1 >= sx2) {
            throw new IllegalArgumentException("sx1 >= sx2");
        }
        if (dy1 >= dy2) {
            throw new IllegalArgumentException("dy1 >= dy2");
        }
        if (sy1 >= sy2) {
            throw new IllegalArgumentException("sy1 >= sy2");
        }

        append("gsave");

        int width = sx2 - sx1;
        int height = sy2 - sy1;
        int destWidth = dx2 - dx1;
        int destHeight = dy2 - dy1;
        
        int[] pixels = new int[width * height];
        PixelGrabber pg = new PixelGrabber(img, sx1, sy1, sx2 - sx1, sy2 - sy1, pixels, 0, width);
        try {
            pg.grabPixels();
        }
        catch(InterruptedException e) {
            return false;
        }

        AffineTransform matrix = new AffineTransform(_transform);
        matrix.translate(dx1, dy1);
        matrix.scale(destWidth / (double) width, destHeight / (double) height);
        double[] m = new double[6];
        try {
            matrix = matrix.createInverse();
        }
        catch (Exception e) {
            throw new EpsException("Unable to get inverse of matrix: " + matrix);
        }
        matrix.scale(1, -1);
        matrix.getMatrix(m);
        append(width + " " + height + " 8 [" + m[0] + " " + m[1] + " " + m[2] + " " +  m[3] + " " + m[4] + " " + m[5] + "]");
        // Fill the background to update the bounding box.
        Color oldColor = getColor();
        setColor(getBackground());
        fillRect(dx1, dy1, destWidth, destHeight);
        setColor(oldColor);
        append("{currentfile 3 100 mul string readhexstring pop} bind");
        append("false 3 colorimage");
        StringBuffer line = new StringBuffer();
        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                Color color = new Color(pixels[x + width * y]);
                line.append(toHexString(color.getRed()) + toHexString(color.getGreen()) + toHexString(color.getBlue()));
                if (line.length() > 64) {
                    append(line.toString());
                    line = new StringBuffer();
                }
            }
        }
        if (line.length() > 0) {
            append(line.toString());
        }
        
        append("grestore");
        
        return true;
    }
    
    
    /**
     * Disposes of all resources used by this EpsGraphics2D object.
     * If this is the only remaining EpsGraphics2D instance pointing at
     * a EpsDocument object, then the EpsDocument object shall become
     * eligible for garbage collection.
     */
    public void dispose() {
        _document = null;
    }
    
    
    /**
     * Finalizes the object.
     */
    public void finalize() {
        super.finalize();
    }
    
    
    /**
     * Returns the entire contents of the EPS document, complete with
     * headers and bounding box.  The returned String is suitable for
     * being written directly to disk as an EPS file.
     */
    public String toString() {
        StringWriter writer = new StringWriter();
        try {
            _document.write(writer);
        }
        catch (IOException e) {
            throw new EpsException(e.toString());
        }
        return writer.toString();
    }
    
    
    /**
     * Returns true if the specified rectangular area might intersect the
     * current clipping area.
     */
    public boolean hitClip(int x, int y, int width, int height) {
        if (_clip == null) {
            return true;
        }
        Rectangle rect = new Rectangle(x, y, width, height);
        return hit(rect, _clip, true);
    }
    
    
    /**
     * Returns the bounding rectangle of the current clipping area.
     */
    public Rectangle getClipBounds(Rectangle r) {
        if (_clip == null) {
            return r;
        }
        Rectangle rect = getClipBounds();
        r.setLocation((int) rect.getX(), (int) rect.getY());
        r.setSize((int) rect.getWidth(), (int) rect.getHeight());
        return r;
    }
    
    
    private Color _color;
    private Color _backgroundColor;
    private Paint _paint;
    private Composite _composite;
    private BasicStroke _stroke;
    private Font _font;
    private Shape _clip;
    private AffineTransform _transform;
    private boolean _accurateTextMode;

    private EpsDocument _document;

    private static FontRenderContext _fontRenderContext = new FontRenderContext(null, false, true);
}